Method Swizzle
Method Swizzle是基于runtime实现“黑魔法”。
写这篇主要是源于上周一次项目bug fix。
项目基于React Native 0.20版本开发,在调用相机拍照后,由于照片自身带有了旋转信息,因此照片在客户看来不是“正的”。
1  | - (void)imagePickerController:(UIImagePickerController *)picker  | 
可以看到其实就是需要对originalImage进行方向修正。
然后跟踪了RN的实现,发现他是存在应用的临时变量里面,可是用的是CGImage。意味着我们即使取出来了这个Image,也无法获知这个Image的方向信息。当时第一反应是。。我自己来实现一个选择器。。。不过这个工作量略大,而且可能会造成很多不知原因的坑。晚上洗澡的时候,突然想到可以可以通过Method swizzle的方式去实现。
新建了一个category,在+(void)load方法的替换了imagePickerController方法,测试一下,OK。
什么时候用
Method Swizzle是把锋利的刀,用的好,削铁如泥;用的不好,害人害己。
一般而言,如果能有好的方法解决,不推荐使用Method Swizzle。原因就是难以跟踪问题。如A实现一个方法run。一个开发在A(B)这个category里面替换成了runFast,而另一个开发在A(C)里面替换成了runSlow。那我们调用run方法的时候到底是什么结果?
该怎么用
替换方法应该是在运行时确定唯一的,如果存在多次不确定的Method Swizzle,我们就无法知道最后获取的IMP来源于来个方法。因此在哪里替换,怎么替换,对于不同类型的方法都不一样。
普通实例方法
替换普通实例方法比较简单,创建了一个对应类的分类,在分类中实现+(void)load方法,在+(void)load方法中进行替换。
1  | + (void)load {  | 
首先要知道为何在+(void)load中实现替换。+(void)load这个方法首先是在运行时执行,切只执行一次,因此就符合了我们在在程序运行期只执行一次替换的想法。其次,+(void)load的执行顺序是父类->子类->分类的顺序,且不覆盖。因此,分类的+(void)load不会影响类的+(void)load也是我们正需要的。
类方法
实现类方法的实现思路也是一样的,不同的是我们不从实例方法列表中去获取相关方法实现
1  | + (void)load {  | 
区别有2点
class_getClassMethod(Class cls, SEL name)替换掉class_getInstanceMethod(Class cls, SEL name)。看得出方法的差异。- 实例方法的内容是记录在class的method list上的,而类方法是记录在meta-class 上的。
 
修改类簇
1  | + (void)load {  | 
官方文档
中详细讲解了什么是类簇。这里我们替换的是__NSDictionaryM中对应的setObject:forKey:
AOP 和 Method Swizzle
有一定开发经验的人一定听说过AOP。用一句个人觉得比较经典的话来概括这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。这里来看,通过Method Swizzle正好来实现AOP。在这方面,github上有一个实现非常好的开源库Aspects
这里的话,会用它来做一下分析,怎么去实现AOP。
功能分析
Aspects这个库实现了AOP,那么实现到什么地步,能做到什么样的功能,可以从头文件定义中略知一二
1  | typedef NS_OPTIONS(NSUInteger, AspectOptions) {  | 
定义了AspectOptions,从名字看出,分别可以做到将新方法插入到老方法之前/之后,替换原有的方法,仅在第一次替换原来的方法。
1  | @protocol AspectToken <NSObject>  | 
两个协议,实现后可以实现撤销插入/ 获得插入的实例信息,原有方法内容和参数列表。
1  | @interface NSObject (Aspects)  | 
对NSObject定义了一个分类,只要两个方法,分别是对类方法和实例方法的操作。
可以看到,Aspect的实现功能还是很强大的。在提供基本Method swizzle的基础上还实现了对不同插入位置的功能,提供可撤回的替换。
实现细节
在自己实现Method Swizzle的时候,我们也会考虑到,如果我需要恢复被替换的方法怎么做?如果我仅仅想在某个方法执行前或执行后执行一个方法呢?比如我需要在所有的viewDidLoad中插入一个log语句,这时候用Method Swizzle显然是不合适,而Aspect能做到的,也是让我们好奇的,来看看具体的实现方法。
1  | + (id<AspectToken>)aspect_hookSelector:(SEL)selector  | 
两个公开API进来后都是调用static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error)这个静态方法,区别就是对类方法中替换要在self前用id修饰。
1  | static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {  | 
方法中首先定义了一个AspectIdentifier类型的实例变量:
1  | @interface AspectIdentifier : NSObject  | 
可以看到这个类的定义就是对Aspect定义的。
随后执行aspect_performLocked:
1  | static void aspect_performLocked(dispatch_block_t block) {  | 
方法中创建了一个OSSpinLockLock自旋锁,对block执行进行保护。
自旋锁是在多处理器系统(SMP)上为保护一段关键代码的执行或者关键数据的一种保护机制,是实现synchronization的一种手段。
传入的block中,首先先执行aspect_isSelectorAllowedAndTrack方法
aspect_isSelectorAllowedAndTrack比较长,从字面含义上来讲就是是否允许插入和追踪。
代码就不贴了。。简单说下思路。
我们知道有些方法是无法被替换的,有些hook的方法对插入的位置很敏感(好像很污的感觉)。这个方法就是对这些黑名单进行判断
首先是不能替换的,有release,retain,autorelease,forwardInvocation
只能在添加block到hook方法前的:dealloc
被hook的类响应SEL的。。。喂你再去检查下好吧
接下来被hook的是不是元类,如果不是的话就可以愉快的返回YES啦。
如果是元类。。稍微麻烦点。梳理一下逻辑如下
swizzledClassesDict是一个dictionary,里面存放的已经是以当前类为key,以AspectTracker为value的键值对。AspectTracker定义如下:
1  | @interface AspectTracker : NSObject  | 
意义如名字一样,实现的是一个跟踪对象。这个对象里面存放了跟踪的类,类的名字,选择器的名称已经子类跟踪对象的选择器。
获得tracker后,判断是否已经hook了子类的相同选择器方法,注意只能在继承链上hook一次相同选择器的方法。
随后递归父类,查看是否在继承链上已经hook过了。
如果上述过程都能顺利进行下来的话,说明可以hook啦,这时候递归父类,将selector添加到tracker里面。
回到上面的aspect_performLocked中,这时候我们得知是能hook啦,这时候根据传进来的self和selector创建一个AspectsContainer。AspectsContainer的定义如下:
1  | @interface AspectsContainer : NSObject  | 
这里有三个属性,都是array类型,从名字不难看到,存的是hook前,被hook的,hook后的。
创建AspectsContainer之后,对identifier初始化,如果成功初始化,向AspectsContainer添加identifier。
1  | - (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {  | 
判断option,将AspectIdentifier添加到对应的array
最后,执行aspect_prepareClassAndHookSelector
aspect_prepareClassAndHookSelector是整个Aspect里面最核心的部分了,前面的行为都是判断是否能hook和做相应的缓存操作,在这里才是真正的执行hook的地方。
1  | static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {  | 
思路和我们自己动手实现Method Swizzle是差不多的,过程也是获取到被hook的class,然后通过selector获取到指定函数指针IMP。然后将传入的方法(block)替换掉IMP。
首先执行的是一个aspect_hookClass方法,返回一个Class对象。
整体的思路如下
1  | Class statedClass = self.class;  | 
如果对runtime不熟悉的人可能不知道这两者的有什么不同。简单的说,self.class返回的是这个Object
所属的类,而object_getClass返回的是这个Object的元类,也就是类对象的类(很绕口)。接下来,就来判断元类是否被修改过(元类的类名被添加了特有的后缀),如果没有修改过,将对象的类进行hook:
1  | static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) {  | 
理解起来不困难,用一个mutableSet存储类名,如果swizzledClasses不在set里面的话,执行aspect_swizzleForwardInvocation。这个方法就是替换forwardInvocation这方法,目的是替换掉forwardInvocation方法转发,采用自定以的__aspects_forwardInvocation
核心就是下面两句:
1  | IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");  | 
__ASPECTS_ARE_BEING_CALLED__是最关键的方法,这个方法就是决定我们要替换的方法如何执行的地方。
1  | static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {  | 
这个方法里,实际上替换的就是forwardInvocation:这个runtime方法,自造了一个调用方法。
目的就是从原有方法和hook方法去做处理。
过程就是先用临时变量获取invocation的seletor,将invocation的selecor替换成aliasSelector。通过传入的参数构造AspectInfo。取得objectContainer和classContainer(都是AspectsContainer)类型的。调用aspect_invoke
aspect_invoke是一个宏方法(其实这里也不用写成宏)。
1  | #define aspect_invoke(aspects, info) \  | 
调用了invokeWithInfo判断aspect的option,如果需要一出,就讲他从对应的container中移除。invokeWithInfo的方法如下:
1  | - (BOOL)invokeWithInfo:(id<AspectInfo>)info {  | 
这个部分就是从原有方法中取出参数列表赋给block,在这过程总检查是否block不符合原来方法。将self.block设为blockInvocation的target。
回到aspect_hookClass,在上面特殊情况处理之后,就是一般情况,这时候创建动态子类,类名以AspectsSubclassSuffix为指定后缀。aspect_hookedGetClass中替换掉Class方法,使其返回的是statedClass(被hook的类),这里将动态生成的子类的Class和Meta Class都替换成statedClass,最后将subclass注册进去,最后将subclass设置成self的类。这样就完成了hook的过程,self执行的方法和信息都被我们hook到了。
回到aspect_prepareClassAndHookSelector(感觉从很深的子树回来真的很不容易),接下来我们获得了我们要hook的Method和IMP,我们要判断是否有对应的IMP
1  | static BOOL aspect_isMsgForwardIMP(IMP impl) {  | 
_objc_msgForward_stret和_objc_msgForward的区别在这篇文章里面有讲解,简单的引用JSPatch作者的解释
大多数CPU在执行C函数时会把前几个参数放进寄存器里,对 obj_msgSend 来说前两个参数固定是 self / _cmd,它们会放在寄存器上,在最后执行完后返回值也会保存在寄存器上,取这个寄存器的值就是返回值。普通的返回值(int/pointer)很小,放在寄存器上没问题,但有些 struct 是很大的,寄存器放不下,所以要用另一种方式,在一开始申请一段内存,把指针保存在寄存器上,返回值往这个指针指向的内存写数据,所以寄存器要腾出一个位置放这个指针,self / _cmd 在寄存器的位置就变了。objc_msgSend 不知道 self / _cmd 的位置变了,所以要用另一个方法 objc_msgSend_stret 代替。原理大概就是这样。在 NSMethodSignature 的 debugDescription 上打出了是否 special struct,只能通过这字符串判断。所以最终的处理是,在非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否则走 _objc_msgForward。
这里如果IMP == _objc_msgForward,说明找不到 class / selector 对应的 IMP,如果能找到的话,我们就可以进行下一步了。
首先要创建一个aliasSelector选择器,用这个选择器去添加targetMethod对应的IMP。这个目的么,自然是保存下来将要被替换方法的实现。
接下来,class_replaceMethod替换掉我们需要hook的方法,使用了aspect_getMsgForwardIMP如下:
1  | static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) {  | 
这里对arm64做了特殊处理,原因就是上文提到实现问题,对返回的struct做特殊的处理。
至此,整个hook的过程就结束了,我们已经将需要hook的方法替换成了我们需要的实现。
回顾
写完后才发现整个思路虽然很清晰,但是跨越很大,如果去处理这个hook的类和方法需要做许多判断,然后将原有的类和方法的信息保存下来,以便于恢复,最后通过class_replaceMethod的方法替换掉了forwardInvocation:。这样通过实现了最后消息转发过程中hook,执行我们注入的方法了。
至于恢复被hook的方法,思路也很简单了,从cache中获取origin method,替换掉hook方法就行了。
总结
Aspect的实现是基于对runtime强大的理解,通过hookforwardInvocation方法,做到了对消息转发的改变,任何对象不能处理的方法最后都会到forwardInvovation中,在这里我们能执行hook的方法和选择执行的时间,也达到了AOP的思想。
Method Swizzle也好,Aspect也好,都是依赖对runtime的认识理解,尤其是Aspect,在学习代码的过程也增长了自己对语言的认知和理解。
在此,项目中也用上了JSPatch,试想如果结合Aspect,通过runtime强大的功能,我们几乎能做到任何时间(坏坏的事情不要想哦)。未来如果有很好的用例,也会分享出来。